/*eslint-env browser*/
/**
 * Script injected by the VS Code Live Preview Extension.
 * http://aka.ms/live-preview
 */

window.addEventListener('message', (event) => handleMessage(event), false);
window.addEventListener('error', (event) => handleError(event), false);

document.addEventListener('DOMContentLoaded', function (e) {
	onLoad();
});

console.error = createConsoleOverride('ERROR');

console.log = createConsoleOverride('LOG');

console.warn = createConsoleOverride('WARN');

console.info = createConsoleOverride('INFO');

console.clear = createConsoleOverride('CLEAR');

/**
 * @description run initialization on load.
 */
function onLoad() {
	let connection = new WebSocket('${WS_URL}');
	connection.onmessage = (event) => handleSocketMessage(event.data);

	let ctrlDown = false;

	const commandPayload = {
		path: window.location,
		title: document.title,
	};

	// In embedded preview, tell the webview panel which page it is on now.
	postParentMessage({
		command: 'update-path',
		text: JSON.stringify(commandPayload),
	});

	handleLinkHoverEnd();

	const links = document.getElementsByTagName('a');
	for (const i in links) {
		// In embedded preview, all link clicks must be checked to see if the target page can be injected with this file's script.
		links[i].onclick = (e) => handleLinkClick(e.target.href);
		links[i].onmouseenter = (e) => handleLinkHoverStart(e.target.href);
		links[i].onmouseleave = handleLinkHoverEnd;
	}

	document.addEventListener('keydown', (e) => {
		ctrlDown = e.ctrlKey || e.metaKey;
		if ((e.key == 'F' || e.key == 'f') && ctrlDown) {
			postParentMessage({
				command: 'show-find',
			});
		}
	});

	document.addEventListener('keyup', (e) => {
		ctrlDown = e.ctrlKey || e.metaKey;
	});
}

/**
 * Helper function to insert a `postParentMesssage` call into console function calls.
 * This will also send the printed information to the output channel if in embedded preview.
 * @param {string} type the type of console log (e.g. info, warn, error, etc.).
 */
function createConsoleOverride(type) {
	// Override console messages to allow the user to see console messages in the output channel (embedded preview only).
	const consoleOverrides = {
		ERROR: console.error,
		LOG: console.log,
		WARN: console.warn,
		INFO: console.info,
		CLEAR: console.clear,
	};
	return function (msg) {
		let stringifiedMsg = 'undefined';

		try {
			stringifiedMsg = JSON.stringify(msg);
			if (!stringifiedMsg) throw new Error('message is not in JSON format');
		} catch (err) {
			try {
				stringifiedMsg = msg.toString();
			} catch (err) {
				// noop
			}
		}

		const messagePayload = {
			type: type,
			data: stringifiedMsg,
		};
		postParentMessage({
			command: 'console',
			text: JSON.stringify(messagePayload),
		});
		consoleOverrides[type].apply(console, arguments);
	};
}

/**
 * Handle reload requests from WebSocket server.
 * @param {any} data
 */
function handleSocketMessage(data) {
	const parsedMessage = JSON.parse(data);
	switch (parsedMessage.command) {
		case 'reload': {
			window.location.reload();
		}
	}
}

/**
 * Handle messages from the parent (specifically for embedded preview).
 * @param {any} event
 */
function handleMessage(event) {
	const message = event.data;

	switch (message.command) {
		case 'refresh':
			window.location.reload();
			break;
		case 'setup-parent-listener': {
			const commandPayload = {
				path: window.location,
				title: document.title,
			};

			postParentMessage({
				command: 'update-path',
				text: JSON.stringify(commandPayload),
			});
			break;
		}
		case 'find-next': {
			let findResult = window.find(message.text);
			if (!findResult) {
				if (hasFindResults(message.text)) {
					findToBeginning(message.text);
					findResult = true;
				}
			}
			postParentMessage({
				command: 'show-find-icon',
				text: findResult,
			});
			break;
		}
		case 'find-prev': {
			let findResult = window.find(message.text, false, true);
			if (!findResult) {
				if (hasFindResults(message.text)) {
					findToEnd(message.text);
					findResult = true;
				}
			}
			postParentMessage({
				command: 'show-find-icon',
				text: findResult,
			});
			break;
		}
		default: {
			if (
				event.data.command != 'perform-url-check' &&
				event.data.command != 'update-path'
			) {
				postParentMessage(event.data);
			}
		}
	}
}

/**
 * Handle errors from the parent (specifically for embedded preview).
 * @param {any} event
 */
function handleError(event) {
	const stackMessage = event.error.stack;
	// stackMessages given in the form:
	//    "errorType: errorMessage"
	// Example:
	//    "SyntaxError: Illegal newline after throw"
	const errorType = stackMessage.split(':')[0];

	// ignore errors such as SyntaxError, ReferenceError, etc
	if (errorType === 'Error') {
		const messagePayload = {
			type: 'UNCAUGHT_ERROR',
			data: stackMessage,
		};
		postParentMessage({
			command: 'console',
			text: JSON.stringify(messagePayload),
		});
	}
}

/**
 * @param {string} searchString the string to search for.
 * @returns whether this string has find results on the page.
 */
function hasFindResults(searchString) {
	window.getSelection().removeAllRanges();
	const canGoForward = window.find(searchString);
	const canGoBack = window.find(searchString, false, true);
	return canGoForward || canGoBack;
}

/**
 * @param {string} searchString the string to search for.
 * @returns move the find position to the beginning of the page.
 */
function findToBeginning(searchString) {
	window.getSelection().removeAllRanges();
	window.find(searchString);
}

/**
 * @param {string} searchString the string to search for.
 * @returns move the find position to the end of the page.
 */
function findToEnd(searchString) {
	window.getSelection().removeAllRanges();
	window.find(searchString, false, true);
}

/**
 * Send message to the parent frame if this is an iframe (specifically for embedded preview).
 * @param {any} data
 */
function postParentMessage(data) {
	if (window.parent !== window) {
		window.parent.postMessage(data, '*');
	}
}

/**
 * @description Monitor link clicks for non-injectable files (files that cannot be injected with this script) or for external links.
 * Primarily for embedded previews.
 * @param {string} linkTarget
 */
function handleLinkClick(linkTarget) {
	const host = '${HTTP_URL}';
	if (linkTarget && linkTarget != '') {
		if (!linkTarget.startsWith(host)) {
			// The embedded preview does not support external sites; let the extension know that an external link has been
			// opened in the embedded preview; this will open the modal to ask the user to navigate in an external browser
			// and will force the embedded preview back to the previous page.
			postParentMessage({ command: 'open-external-link', text: linkTarget });
		} else {
			// Check all local URLs to make sure to catch pages that won't be injectable
			postParentMessage({ command: 'perform-url-check', text: linkTarget });
		}
	}
}

/**
 * @description Show link preview on embedded preview.
 * @param {string} linkTarget
 */
function handleLinkHoverStart(linkTarget) {
	// In embedded preview, trigger the link preview.
	postParentMessage({
		command: 'link-hover-start',
		text: linkTarget,
	});
}

/**
 * @description Hide link preview on embedded preview.
 */
function handleLinkHoverEnd() {
	postParentMessage({
		command: 'link-hover-end',
	});
}
